package org.tulkas.robot.http;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.ProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.cache.ResourceFactory;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.cache.CacheConfig;
import org.apache.http.impl.client.cache.CachingHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tulkas.robot.base.FlusherManager;
import org.tulkas.robot.base.GlobalConfig;
import org.tulkas.robot.base.IFlusher;
import org.tulkas.robot.base.IManager;
import org.tulkas.robot.exception.SystemException;
import org.tulkas.util.FileUtil;

/**
 * @author <a href="mailto:tulkas.web@gmail.com">Tulkas</a>
 *
 */
public class ClientManager implements IManager {
	public static final String CONNECTION_CONTEXT_ATTR = "tulkas-request_headers";

	private static final String CLIENTS_DIR_NAME = "clients";
	private static final Pattern PTN_CLIENT_NAME = Pattern.compile("[\\w-]+");

	private static Logger logger = LoggerFactory.getLogger(ClientManager.class);

	private volatile static ClientManager instance = null;

	private final Map<String, ClientOptions> configs = new ConcurrentHashMap<String, ClientOptions>();
	private final Map<String, CookieStorage> cookies = new ConcurrentHashMap<String, CookieStorage>();
	private final Map<String, CacheStorage> caches = new ConcurrentHashMap<String, CacheStorage>();
	private final Map<String, HttpClient> clients = new ConcurrentHashMap<String, HttpClient>();

	private final ThreadSafeClientConnManager connManager;
	private final ConnectionMonitor monitor;
	private final ClientFlusher flusher;

	protected ClientManager() {
		SchemeRegistry sr = new SchemeRegistry();
		sr.register(new Scheme("http", 80, PlainSocketFactory
				.getSocketFactory()));
		sr.register(new Scheme("https", 443, SSLSocketFactory
				.getSocketFactory()));
		connManager = new ThreadSafeClientConnManager(sr);
		connManager.setMaxTotal(GlobalConfig.getInstance().getInt(
				"client.manager.conn.maxTotal", 100));
		connManager.setDefaultMaxPerRoute(GlobalConfig.getInstance().getInt(
				"client.manager.conn.maxPerRoute", 5));
		monitor = new ConnectionMonitor();
		monitor.start();

		flusher = new ClientFlusher();
		FlusherManager.getInstance().addFlusher(
				flusher,
				GlobalConfig.getInstance().getInt(
						"client.manager.flusher.interval", 60));
	}

	public static ClientManager getInstance() {
		if (instance == null)
			synchronized (ClientManager.class) {
				if (instance == null) {
					instance = new ClientManager();
				}
			}
		return instance;
	}

	public void destroy() {
		monitor.stop();
		connManager.shutdown();

		FlusherManager.getInstance().removeFlusher(flusher);
		synchronized (ClientManager.class) {
			instance = null;
		}
	}

	public String getClientsRootPath() {
		return GlobalConfig.getInstance().getDataPath() + "/"
				+ CLIENTS_DIR_NAME;
	}

	public String getClientPath(String clientName) {
		return getClientsRootPath() + "/" + clientName;
	}

	protected void checkClientName(String clientName) {
		if (!PTN_CLIENT_NAME.matcher(clientName).matches())
			throw new SystemException("Unavailable characters in clientName '"
					+ clientName + "', Only a-z/A-Z/0-9/-_ are avaliable");
	}

	public ClientOptions getClientOptions(String clientName) {
		checkClientName(clientName);
		ClientOptions ret = configs.get(clientName);
		if (ret == null) {
			synchronized (configs) {
				if (!configs.containsKey(clientName)) {
					ret = new ClientOptions(clientName);
					configs.put(clientName, ret);
				}
			}
		}
		return ret;
	}

	public HttpClient getHttpClient(String clientName) {
		checkClientName(clientName);
		HttpClient ret = clients.get(clientName);
		if (ret == null) {
			synchronized (clients) {
				if (!clients.containsKey(clientName)) {
					ret = prepareHttpClient(clientName);
					clients.put(clientName, ret);
				}
			}
		}

		return ret;
	}

	public CookieStorage getCookieStorage(String clientName) {
		checkClientName(clientName);
		CookieStorage ret = cookies.get(clientName);
		if (ret == null) {
			synchronized (cookies) {
				if (!cookies.containsKey(clientName)) {
					ret = new CookieStorage(clientName);
					ret.setPersistent(getClientOptions(clientName)
							.isCookiePersist());
					cookies.put(clientName, ret);
				}
			}
		}
		return ret;
	}

	public CacheStorage getCacheStorage(String clientName) {
		checkClientName(clientName);
		CacheStorage ret = caches.get(clientName);
		if (ret == null) {
			synchronized (caches) {
				if (!caches.containsKey(clientName)) {
					ret = new CacheStorage(clientName, getClientOptions(
							clientName).getCacheConfig());
					caches.put(clientName, ret);
				}
			}
		}
		return ret;
	}

	public void deleteClient(String clientName) {
		destroyClient(clientName);
		String path = getClientPath(clientName);
		FileUtil.delTree(path);
	}

	public void destroyClient(String clientName) {
		clients.remove(clientName);
		configs.remove(clientName);
		cookies.remove(clientName);
		caches.remove(clientName);
	}

	public void resetHttpClient(String clientName) {
		clients.remove(clientName);
	}

	public void deleteAllClients() {
		String[] clients = getAllClientNames();
		for (String name : clients) {
			deleteClient(name);
		}
	}

	public String[] getLoadedClientNames() {
		TreeSet<String> set = new TreeSet<String>();
		set.addAll(configs.keySet());
		set.addAll(cookies.keySet());
		set.addAll(caches.keySet());
		String[] ret = new String[set.size()];
		set.toArray(ret);
		return ret;
	}

	public String[] getAllClientNames() {
		String clientsDir = getClientsRootPath();
		File dir = new File(clientsDir);
		if (!dir.exists()) {
			return new String[0];
		}
		File[] files = dir.listFiles();
		TreeSet<String> set = new TreeSet<String>();
		for (File file : files) {
			if (file.isDirectory())
				set.add(file.getName());
		}

		String[] loaded = getLoadedClientNames();
		for (String name : loaded) {
			set.add(name);
		}

		String[] ret = new String[set.size()];
		set.toArray(ret);
		return ret;
	}

	protected HttpClient prepareHttpClient(String clientName) {
		ClientOptions options = getClientOptions(clientName);

		DefaultHttpClient client = null;
		client = new DefaultHttpClient(connManager);

		if (logger.isDebugEnabled()) {
			client.addRequestInterceptor(new HttpRequestInterceptor() {
				public void process(final HttpRequest request,
						final HttpContext context) throws HttpException,
						IOException {
					BaseConn conn = (BaseConn) context
							.getAttribute(CONNECTION_CONTEXT_ATTR);
					if (conn != null)
						conn.getRequestHeaders().setHeaders(
								request.getAllHeaders());
				}
			});
		}

		client.setRedirectStrategy(new RedirectStrategy());

		HttpClient ret = client;
		if (options.isCookieEnabled()) {
			client.setCookieStore(getCookieStorage(clientName));
		}
		if (options.isCacheEnabled()) {
			CacheStorage storage = getCacheStorage(clientName);
			ResourceFactory resFactory = storage.getResourceFactory();
			CacheConfig config = options.getCacheConfig();
			ret = new CachingHttpClient(client, resFactory, storage, config);
		}

		prepareHttpParameters(ret.getParams(), options);

		return ret;
	}

	protected void prepareHttpParameters(HttpParams params,
			ClientOptions options) {

		params.setParameter(CoreProtocolPNames.USER_AGENT, options
				.getUserAgent());
		HttpHost proxy = options.getProxyHost();
		if (proxy != null) {
			params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
		}

		if (options.isCookieEnabled()) {
			params.setParameter(ClientPNames.COOKIE_POLICY,
					CookiePolicy.BEST_MATCH);
		}

		String[] keys = options.getHttpParamKeys();
		for (String key : keys) {
			params.setParameter(key, options.getHttpParam(key));
		}
	}

	/**
	 * this class override the default RedirectStrategy for HttpClient. To make 302 & 307 response effective in POST messages.
	 *
	 */
	static class RedirectStrategy extends DefaultRedirectStrategy {
		public boolean isRedirected(final HttpRequest request,
				final HttpResponse response, final HttpContext context)
				throws ProtocolException {
			boolean ret = super.isRedirected(request, response, context);
			if (!ret) {
				int statusCode = response.getStatusLine().getStatusCode();
				String method = request.getRequestLine().getMethod();
				if (HttpStatus.SC_MOVED_TEMPORARILY == statusCode
						&& HttpPost.METHOD_NAME.equals(method))
					ret = true;
				else if (HttpStatus.SC_TEMPORARY_REDIRECT == statusCode
						&& HttpPost.METHOD_NAME.equals(method))
					ret = true;
			}
			if (ret) {
				BaseConn conn = (BaseConn) context
						.getAttribute(CONNECTION_CONTEXT_ATTR);
				conn.setRedirected(true);
			}

			return ret;
		}

		@Override
		public HttpUriRequest getRedirect(HttpRequest request,
				HttpResponse response, HttpContext context)
				throws ProtocolException {
			HttpUriRequest req = super.getRedirect(request, response, context);
			if (req != null) {
				int code = response.getStatusLine().getStatusCode();
				if (HttpStatus.SC_MOVED_PERMANENTLY == code
						|| HttpStatus.SC_MOVED_TEMPORARILY == code
						|| HttpStatus.SC_SEE_OTHER == code
						|| HttpStatus.SC_TEMPORARY_REDIRECT == code) {
					BaseConn conn = (BaseConn) context
							.getAttribute(CONNECTION_CONTEXT_ATTR);
					conn.setUrl(req.getURI().toString());
				}
			}
			return req;
		}
	}

	class ClientFlusher implements IFlusher {

		public void flush() {
			for (CookieStorage cookie : cookies.values()) {
				if (cookie.isAvaliable() && cookie.isModified())
					cookie.flush();
			}
			for (CacheStorage cache : caches.values()) {
				if (cache.isAvaliable() && cache.isModified())
					cache.flush();
			}
		}

		public boolean isAvaliable() {
			return true;
		}

		public boolean isModified() {
			return true;
		}
	}

	class ConnectionMonitor implements Runnable {
		private boolean running = false;
		private int interval;

		private Thread thread = null;

		public ConnectionMonitor() {
			interval = GlobalConfig.getInstance().getInt(
					"client.manager.monitor.interval", 10) * 1000;
		}

		public synchronized void start() {
			if (!running) {
				running = true;
				thread = new Thread(this, "ConnectionMonitor");
				thread.start();
			}
		}

		public synchronized void stop() {
			this.running = false;
		}

		public synchronized boolean isRunning() {
			return running;
		}

		public void run() {
			while (true) {
				synchronized (this) {
					if (!running)
						break;
				}

				connManager.closeExpiredConnections();
				connManager.closeIdleConnections(30, TimeUnit.SECONDS);

				try {
					Thread.sleep(interval);
				} catch (InterruptedException e) {
				}
			}
		}
	}
}
